3.4 流控制
Go精简(合并)了流控制语句,虽然某些时候不够便捷,但够用。
if…else…
条件表达式值必须是布尔类型,可省略括号,且左花括号不能另起一行。
func main() {
x:=3
if x>5{
println("a")
}else if x<5&&x>0{
println("b")
}else{
println("z")
}
}比较特别的是对初始化语句的支持,可定义块局部变量或执行初始化函数。
func main() {
x:=10
if xinit();x==0{ // 优先执行xinit函数
println("a")
}
if a,b:=x+1,x+10;a<b{ // 定义一个或多个局部变量(也可以是函数返回值)
println(a)
}else{
println(b)
}
}局部变量的有效范围包含整个if/else块。
对于编程初学者,可能会因条件匹配顺序不当而写出死代码(dead code)。
func main() {
x:=8
if x>5{ // 优先判断,条件表达式结果为true
println("a")
}else if x>7{ //dead code
println("b")
}
}输出:
a
死代码是指永远不会被执行的代码,可使用专门的工具,或用代码覆盖率(code coverage)测试进行检查。某些比较智能的编译器也可主动清除死代码(dead code elimination,DCE)。
尽可能减少代码块嵌套,让正常逻辑处于相同层次。
import(
"errors"
"log"
)
func check(x int)error{
if x<=0{
return errors.New("x<=0")
}
return nil
}
func main() {
x:=10
if err:=check(x);err==nil{
x++
println(x)
}else{
log.Fatalln(err)
}
}该示例中,if块显然承担了两种逻辑:错误处理和后续正常操作。基于重构原则,我们应该保持代码块功能的单一性。
func main() {
x:=10
if err:=check(x);err!=nil{
log.Fatalln(err)
}
x++
println(x)
}如此,if块仅完成条件检查和错误处理,相关正常逻辑保持在同一层次。当有人试图通过阅读这段代码来获知逻辑流程时,完全可忽略if块细节。同时,单一功能可提升代码可维护性,更利于拆分重构。
当然,如须在多个条件块中使用局部变量,那么只能保留原层次,或直接使用外部变量。
import(
"log"
"strconv"
)
func main() {
s:= "9"
n,err:=strconv.ParseInt(s,10,64) // 使用外部变量
if err!=nil{
log.Fatalln(err)
}else if n<0||n>10{ // 也可考虑拆分成另一个独立if块
log.Fatalln("invalid number")
}
println(n) // 避免if局部变量将该逻辑放到else块
}对于某些过于复杂的组合条件,建议将其重构为函数。
import(
"log"
"strconv"
)
func main() {
s:= "9"
if n,err:=strconv.ParseInt(s,10,64);err!=nil||n<0||n>10||n%2!=0{
log.Fatalln("invalid number")
}
println("ok")
}函数调用虽然有一些性能损失,可却让主流程变得更加清爽。况且,条件语句独立之后,更易于测试,同样会改善代码可维护性。
import(
"errors"
"log"
"strconv"
)
func check(s string)error{
n,err:=strconv.ParseInt(s,10,64)
if err!=nil||n<0||n>10||n%2!=0{
return errors.New("invalid number")
}
return nil
}
func main() {
s:= "9"
if err:=check(s);err!=nil{
log.Fatalln(err)
}
println("ok")
}将流程和局部细节分离是很常见的做法,不同的变化因素被分隔在各自独立单元(函数或模块)内,可避免修改时造成关联错误,减少患“肥胖症”的函数数量。当然,代码单元测试也是主要原因之一。另一方面,该示例中的函数check仅被if块调用,也可将其作为局部函数,以避免扩大作用域,只是对测试的友好度会差一些。
当前编译器只能说够用,须优化的地方太多,其中内联处理做得也差强人意,所以代码维护性和性能平衡需要投入更多心力。
语言方面,最遗憾的是没有条件运算符“a>b?a:b”。有没有lambda无所谓,但没有这个却少了份优雅。加上一大堆err!=nil判断语句,对于有完美主义倾向的代码洁癖患者来说是种折磨。
switch
与if类似,switch语句也用于选择执行,但具体使用场景会有所不同。
func main() {
a,b,c,x:=1,2,3,2
switch x{ // 将x与case条件匹配
case a,b: // 多个匹配条件命中其一即可(OR),变量
println("a|b")
case c: // 单个匹配条件
println("c")
case 4: // 常量
println("d")
default:
println("z")
}
}输出:
a|b
条件表达式支持非常量值,这要比C更加灵活。相比if表达式,switch值列表要更加简洁。
编译器对if、switch生成的机器指令可能完全相同,所谓谁性能更好须看具体情况,不能作为主观判断条件。
switch同样支持初始化语句,按从上到下、从左到右顺序匹配case执行。只有全部匹配失败时,才会执行default块。
func main() {
switch x:=5;x{
default: // 编译器确保不会先执行default块
x+=100
println(x)
case 5:
x+=50
println(x)
}
}
输出:
55
考虑到default作用类似else,建议将其放置在switch末尾。
相邻的空case不构成多条件匹配。
switch x{
case a: // 单条件,内容为空。隐式 “case a:break;”
case b:
println("b")
}不能出现重复的case常量值。
func main() {
switch x:=5;x{
case 5:
println("a")
case 6,5: // 错误:duplicate case 5 in switch
println("b")
}
}无须显式执行break语句,case执行完毕后自动中断。如须贯通后续case(源码顺序),须执行fallthrough,但不再匹配后续条件表达式。
func main() {
switch x:=5;x{
default:
println(x)
case 5:
x+=10
println(x)
fallthrough // 继续执行下一case,但不再匹配条件表达式
case 6:
x+=20
println(x)
//fallthrough // 如果在此继续fallthrough,不会执行default,完全按源码顺序
} // 导致 "cannot fallthrough final case in switch" 错误
}输出:
15
35
注意,fallthrough必须放在case块结尾,可使用break语句阻止。
func main() {
switch x:=5;x{
case 5:
x+=10
println(x)
if x>=15{
break // 终止,不再执行后续语句
}
fallthrough // 必须是case块的最后一条语句
case 6:
x+=20
println(x)
}
}输出:
15
某些时候,switch还被用来替换if语句。被省略的switch条件表达式默认值为true,继而与case比较表达式结果匹配。
func main() {
switch x:=5; { // 相当于 “switch x:=5;true{ ... }”
case x>5:
println("a")
case x>0&&x<=5: // 不能写成 “case x>0,x<=5”,因为多条件是OR关系
println("b")
default:
println("z")
}
}输出:
b
switch语句也可用于接口类型匹配,详见后续章节。
for
仅有for一种循环语句,但常用方式都能支持。
for i:=0;i<3;i++ { // 初始化表达式支持函数调用或定义局部变量
}
for x<10{ // 类似 "while x<10{}" 或 "for;x<10; {}"
x++
}
for{ // 类似 "while true{}" 或 "for true{}"
break
}初始化语句仅被执行一次。条件表达式中如有函数调用,须确认是否会重复执行。可能会被编译器优化掉,也可能是动态结果须每次执行确认。
func count()int{
print("count.")
return 3
}
func main() {
for i,c:=0,count();i<c;i++ { // 初始化语句的count函数仅执行一次
println("a",i)
}
c:=0
for c<count() { // 条件表达式中的count重复执行
println("b",c)
c++
}
}输出:
count.a 0
a 1
a 2
count.b 0
count.b 1
count.b 2
规避方式就是在初始化表达式中定义局部变量保存count结果。
可用for…range完成数据迭代,支持字符串、数组、数组指针、切片、字典、通道类型,返回索引、键值数据。
data type 1st value 2nd value
-----------------+-----------------+------------------+------------------
string index s[index] unicode,rune
array/slice index v[index]
map key value
channel element
func main() {
data:= [3]string{"a", "b", "c"}
for i,s:=range data{
println(i,s)
}
}输出:
0 a
1 b
2 c
没有相关接口实现自定义类型迭代,除非基础类型是上述类型之一。
允许返回单值,或用“_”忽略。
func main() {
data:= [3]string{"a", "b", "c"}
for i:=range data{ // 只返回1st value
println(i,data[i])
}
for_,s:=range data{ // 忽略1st value
println(s)
}
for range data{ // 仅迭代,不返回。可用来执行清空channel等操作
}
}无论普通for循环,还是range迭代,其定义的局部变量都会重复使用。
func main() {
data:= [3]string{"a", "b", "c"}
for i,s:=range data{
println(&i, &s)
}
}输出:
0xc82003fe98 0xc82003fec8
0xc82003fe98 0xc82003fec8
0xc82003fe98 0xc82003fec8
这对闭包存在一些影响,相关详情,请阅读后续章节。
注意,range会复制目标数据。受直接影响的是数组,可改用数组指针或切片类型。
func main() {
data:= [3]int{10,20,30}
for i,x:=range data{ // 从data复制品中取值
if i==0{
data[0] +=100
data[1] +=200
data[2] +=300
}
fmt.Printf("x: %d,data: %d\n",x,data[i])
}
for i,x:=range data[:] { // 仅复制slice,不包括底层array
if i==0{
data[0] +=100
data[1] +=200
data[2] +=300
}
fmt.Printf("x: %d,data: %d\n",x,data[i])
}
}输出:
x:10,data:110
x:20,data:220 //range返回的依旧是复制值
x:30,data:330
x:110,data:210 // 当i==0修改data时,x已经取值,所以是110
x:420,data:420 // 复制的仅是slice自身,底层array依旧是原对象
x:630,data:630
相关数据类型中,字符串、切片基本结构是个很小的结构体,而字典、通道本身是指针封装,复制成本都很小,无须专门优化。
如果range目标表达式是函数调用,也仅被执行一次。
func data() []int{
println("origin data.")
return[]int{10,20,30}
}
func main() {
for i,x:=range data() {
println(i,x)
}
}输出:
origin data.
0 10
1 20
2 30
建议嵌套循环不要超过2层,否则会难以维护。必要时可剥离,重构为函数。
goto,continue,break
对于goto的讨伐由来已久,仿佛它是“笨蛋”标签一般。可事实上,能在很多场合见到它的身影,就连Go源码里都有很多。
$cd go/src
$grep-r-n"goto" *
单就Go 1.6的源码统计结果,goto语句就超出1000条有余。很惊讶,不是吗?虽然某些设计模式可用来消除goto语句,但在性能优先的场合,它能发挥积极作用。
使用goto前,须先定义标签。标签区分大小写,且未使用的标签会引发编译错误。
func main() {
start: // 错误:label start defined and not used
for i:=0;i<3;i++ {
println(i)
if i>1{
goto exit
}
}
exit:
println("exit.")
}
不能跳转到其他函数,或内层代码块内。
func test() {
test:
println("test")
println("test exit.")
}
func main() {
for i:=0;i<3;i++ {
loop:
println(i)
}
goto test // 错误:label test not defined
goto loop // 错误:goto loop jumps into block
}和goto定点跳转不同,break、continue用于中断代码块执行。
- break:用于switch、for、select语句,终止整个语句块执行。
- continue:仅用于for循环,终止后续逻辑,立即进入下一轮循环。
func main() {
for i:=0;i<10;i++ {
if i%2==0{
continue // 立即进入下一轮循环
}
if i>5{
break // 立即终止整个for循环
}
println(i)
}
}输出:
1
3
5
配合标签,break和continue可在多层嵌套中指定目标层级。
func main() {
outer:
for x:=0;x<5;x++ {
for y:=0;y<10;y++ {
if y>2{
println()
continue outer
}
if x>2{
break outer
}
print(x, ":",y, " ")
}
}
}输出:
0:0 0:1 0:2
1:0 1:1 1:2
2:0 2:1 2:2